﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PServiceBus.Core.Runtime.Messages;
using System.Reflection;
using System.Reflection.Emit;
using PServiceBus.Core.Runtime;
using System.ServiceModel;
using System.ServiceModel.Channels;
using Phoenix.ReflectionExtensions;
using PServiceBus.Core.Runtime.Extensions;
using PServiceBus.Core.Runtime.Serializers;
using System.ServiceModel.Description;
using PServiceBus.Serializer.Xml;

namespace PServiceBus.WCF.NetTcpTransport {
    public static class NetTcpHelper {
        private static readonly string _listStr = String.Intern("list");
        private static readonly string _dictStr = String.Intern("dict");
        private static readonly string _dictKey = String.Intern("dictKey");
        private static readonly string _dictValue = String.Intern("dictValue");
        private static readonly string _dotNetTypeStr = String.Intern("dotNetType");

        private static readonly Dictionary<string, Type> CachedInterface = new Dictionary<string, Type>();
        private const MethodAttributes MethodAttribute =
            MethodAttributes.Public
            | MethodAttributes.Abstract
            | MethodAttributes.Virtual
            | MethodAttributes.HideBySig
            | MethodAttributes.NewSlot;
        private static Type _channelFactoryType = typeof(ChannelFactory<>);
        private static Type _operationContractType = typeof(OperationContractAttribute);

        private static Type GenerateInterfaceFrom(string interfaceName, Dictionary<string, List<KeyValuePair<string,Type>>> methods) {
            var key = interfaceName + String.Join("_", methods.Select(kv => kv.Key)) +
                String.Join("_", methods.Select(kv => String.Join("_",
                    kv.Value.Select(kvp => String.Format("{0}:{1}", kvp.Key, kvp.Value.Name)))));
            if (CachedInterface.ContainsKey(key)) return CachedInterface[key];
            var asmName = interfaceName + "Asm";
            var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(asmName) { Version = new Version(1, 0, 0, 0) },
                                                                     AssemblyBuilderAccess.RunAndSave);
            var module = assembly.DefineDynamicModule(asmName + ".dll");
            var type = module.DefineType(interfaceName, TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract);
            type.SetCustomAttribute(new CustomAttributeBuilder(typeof(ServiceContractAttribute).GetConstructor(Type.EmptyTypes),
                new object[] { }));
            
            foreach (var method in methods) {
                var methodParamKeyValues = method.Value;
                var methodBuilder = type.DefineMethod(method.Key, MethodAttribute,typeof(void),
                    methodParamKeyValues.Select(kv => kv.Value).ToArray());

                var counter = 0;
                foreach (var paramKv in methodParamKeyValues) {
                    methodBuilder.DefineParameter(++counter, ParameterAttributes.None, paramKv.Key);
                }
                methodBuilder.SetCustomAttribute(
                    new CustomAttributeBuilder(_operationContractType.GetConstructor(Type.EmptyTypes), new object[] { }));
            }

            var returnType = type.CreateType();
            CachedInterface[key] = returnType;
            return returnType;
        }

        private static Type GenerateClassTypeFromDict(Dictionary<string, object> dict){
            if (dict.ContainsKey(_dotNetTypeStr)) {
                var dotNetType = dict[_dotNetTypeStr].ToString();
                var type = default(Type);
                if (dotNetType == _listStr) {
                    var listDict = dict[_listStr] as Dictionary<string, object>;
                    type = typeof(List<>).MakeGenericType(GenerateClassTypeFromDict(listDict));
                } else if (dotNetType == _dictStr) {
                    var keyDict = dict[_dictKey] as Dictionary<string, object>;
                    var valueDict = dict[_dictValue] as Dictionary<string, object>;
                    type = typeof(Dictionary<,>).MakeGenericType(GenerateClassTypeFromDict(keyDict),
                        GenerateClassTypeFromDict(valueDict));
                } else type = Type.GetType(dotNetType);
                return type;
            }
            return ReflectionHelper.GenerateClassType(null, dict);
        }

        public static void Invoke(NetTcpTransport transport, MessageMapper messageMapper) {
            var operationContext = transport.OperationContext;
            var operationName = operationContext.OperationName;
            var methodParameterTypes = operationContext.ParameterInfos
                .Select(paramDict => 
                    GenerateClassTypeFromDict(paramDict.FromJson<Dictionary<string, object>>()))
                .ToList();
            
            var interfaceType = GenerateInterfaceFrom(operationContext.ServiceType,
                new Dictionary<string, List<KeyValuePair<string,Type>>>() { { operationName, 
                          methodParameterTypes
                          .Select((type, index) 
                              => new KeyValuePair<string, Type>(transport.MethodParameters[index].Name, type) ).ToList() } });

                
            var proxy = GenerateProxy(interfaceType, transport);
            var methodInfo = interfaceType.GetMethodEx(operationName);
            var proxyDisposable = proxy as IDisposable;
            foreach (var msgToken in messageMapper.Tokens) {
                var methodParams = GetMethodParams(transport, msgToken, methodParameterTypes);
                methodInfo.Invoke(proxy, methodParams);
            }
            if (proxyDisposable != null) proxyDisposable.Dispose();
        }

        private static object[] GetMethodParams(NetTcpTransport transport, MessageMapperToken token, List<Type> methodParameterTypes) {
            var result = new List<object>();
            var methodParameters = transport.MethodParameters;
            for (var i = 0; i < methodParameters.Count; i++) {
                var paramType = methodParameterTypes[i];
                var paramName = methodParameters[i].MessagePropertyName;
                var value = token[paramName];
                result.Add(!paramType.CanInitType() ? value.ChangeType(paramType) :
                    XmlSerializer.Deserialize(String.Format("<{0}>{1}</{0}>", paramName, value), paramType));
            }
            return result.ToArray();
        }

        private static object GenerateProxy(Type interfaceType, NetTcpTransport transport) {
            var binding = transport.Binding;
            var operationContext = transport.OperationContext;
            var netTcpBinding = new NetTcpBinding(binding.SecurityMode)
            {
                TransferMode = binding.TransferMode,
                MaxReceivedMessageSize = binding.MaxReceivedMessageSize,
                MaxBufferSize = binding.MaxBufferSize
            };
            netTcpBinding.ReaderQuotas.MaxArrayLength = binding.MaxArrayLength;
            netTcpBinding.ReaderQuotas.MaxBytesPerRead = binding.MaxBytesPerRead;
            netTcpBinding.ReaderQuotas.MaxStringContentLength = binding.MaxStringContentLength;
            netTcpBinding.ReaderQuotas.MaxNameTableCharCount = binding.MaxNameTableCharCount;
            netTcpBinding.ReaderQuotas.MaxDepth = binding.MaxDepth;

            var channelCtor = _channelFactoryType.MakeGenericType(interfaceType)
                .GetConstructor(new[] { typeof(Binding), typeof(EndpointAddress) });
            var service = channelCtor.Invoke(new object[] { netTcpBinding, new EndpointAddress(transport.EndpointAddress) }) as ChannelFactory;
            var serviceType = service.GetType();
            var serviceEndpoint = service.Endpoint;
            var createChannel = serviceType.GetMethodEx("CreateChannel", Type.EmptyTypes).Get<Func<object, object>>();
            var operation = serviceEndpoint.Contract.Operations.FirstOrDefault(oper => oper.Name == operationContext.OperationName);
            operation.Behaviors.Add(
                new DataSerializerOperationAttribute(true, binding.MaxItemsInObjectGraph));
            return createChannel(service);
        }

    }
}
